@file:Suppress("MemberVisibilityCanBePrivate")

package dev.entao.core

import dev.entao.base.LazyValue
import dev.entao.base.createInstanceX
import dev.entao.base.ensureDirs
import dev.entao.base.resourceText
import dev.entao.core.util.PackageScanner
import dev.entao.json.YsonObject
import dev.entao.log.*
import dev.entao.sql.ConnLook
import java.io.File
import javax.servlet.FilterChain
import javax.servlet.ServletContext
import javax.servlet.annotation.MultipartConfig
import javax.servlet.annotation.WebFilter
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import kotlin.reflect.KClass
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberFunctions
import kotlin.reflect.full.primaryConstructor

@MultipartConfig
@WebFilter(urlPatterns = ["/*"])
class HttpService : HttpFilter() {
    val contextPath: String get() = servletContext.contextPath

    val webappsDir: File by lazy { appDir.parentFile }
    val appDir: File by lazy { File(servletContext.getRealPath("/")) }

    var sessionTimeoutSeconds: Int = 3600

    var dirWork: File by LazyValue { File(webappsDir, contextPath.trim('/') + "_work").ensureDirs() }
    var dirUpload: File by LazyValue { File(dirWork, "uploads").ensureDirs() }
    var dirTemp: File by LazyValue { File(dirWork, "temp").ensureDirs() }
    var logPrinter: LogPrinter by LazyValue {
        LogTree(DirLogPrinter(File(dirWork, "xlog"), 15), ConsoleLogPrinter())
    }

    val routeManager: HttpActionManager by lazy {
        HttpActionManager(servletContext.contextPath)
    }
    private val sliceList: ArrayList<HttpSlice> = ArrayList()

    val infoMap = HashMap<String, Any>()

    val webTempDir: File get() = servletContext.getAttribute(ServletContext.TEMPDIR) as File

    lateinit var app: SweetApp

    private fun loadConfig(): YsonObject {
        val s = this::class.resourceText(APP_JSON) ?: error("app class load failed, please edit /resources/$APP_JSON")
        return YsonObject(s)
    }

    override fun onInit() {
        super.onInit()

        logd("WEB Temp Dir: ", webTempDir.absolutePath)

        StaticRender.publishDir("@/", appDir) {
            this.permission {
                "hole" !in it.absolutePath
            }
        }

        val configs = loadConfig()
        val appCls = configs.getString("app") ?: error("app class load failed, please edit /resources/$APP_JSON")
        app = Class.forName(appCls).kotlin.createInstanceX(this)

        app.onConfig()
        dirWork.ensureDirs()
        dirUpload.ensureDirs()
        dirTemp.ensureDirs()
        LogX.setPrinter(logPrinter)
        LogX.d("init log printer")
        try {
            sliceList.clear()
            val controllers = configs.getString("controllers")
            if (controllers != null && controllers.isNotEmpty()) {
                scanControllers(controllers)
            }
            app.onCreate()
            for (hs in sliceList) {
                hs.onInit(this)
            }

        } catch (ex: Exception) {
            ex.printStackTrace()
        } finally {
            app.onCleanThreadLocal()
        }
    }

    fun cleanThreadLocals() {
        app.onCleanThreadLocal()
    }

    override fun allowMethods(request: HttpServletRequest): Set<String> {
        val r = routeManager.findRouter(request)
        if (r != null) {
            logd("router found! ", r.allowMethods)
        } else {
            logd("router NOT found! ", routeManager.routeMap.keys)
        }
        val set = r?.allowMethods ?: emptySet()
        logd("Allow Methods: ", request.currentUri, " set:", set)
        return set
    }

    override fun getLastModified(req: HttpServletRequest): Long {
        StaticRender.findFile(req)?.also { return it.lastModified() }
        return super.getLastModified(req)
    }

    private fun doRouter(r: Router, c: HttpContext, lsReq: List<HttpSlice>) {
        if (c.request.method.uppercase() !in r.allowMethods) {
            c.response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED)
            return
        }
        logd("Invoke Router: ", r.cls.qualifiedName, r.function.name)
        val ls = ArrayList<HttpSlice>()
        for (a in lsReq) {
            ls += a
            a.beforeRouter(c, r.cls, r.function)
            if (c.isCommited) break
        }
        try {
            if (!c.isCommited) {
                r.dispatch(c)
            }
        } finally {
            for (a in ls) {
                a.afterRouter(c, r.cls, r.function)
            }
        }

    }

    private fun doHttpAction(c: HttpContext, lsReq: List<HttpSlice>) {
        val r = routeManager.findRouter(c)
        if (r != null) {
            doRouter(r, c, lsReq)
            return
        }
        val pair = routeManager.findController(c.currentUri)
        if (pair != null) {
            val cls = pair.second
            val missFun = cls.memberFunctions.firstOrNull { it.hasAnnotation<MissAction>() }
            if (missFun != null) {
                logd("Invoke MissAction: ", cls.qualifiedName, missFun.name)
                val inst = cls.primaryConstructor?.call(c)
                if (inst != null) {
                    lsReq.forEach {
                        it.beforeRouter(c, cls, missFun)
                        if (c.isCommited) return
                    }
                    try {
                        val map = Router.prepareParams(c, inst, missFun)
                        missFun.callBy(map)
                    } finally {
                        lsReq.forEach { it.afterRouter(c, cls, missFun) }
                    }
                    return
                }
            }
        }
        val sr = StaticRender(c)
        if (sr.existFile) {
            sr.send()
            return
        }
        c.nextChain()
    }

    override fun doOptions(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
        allowCross(request, response)
        super.doOptions(request, response, chain)
    }

    override fun doGet(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
        doHttpFilter(request, response, chain)
    }

    override fun doPost(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
        doHttpFilter(request, response, chain)
    }

    override fun doPut(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
        doHttpFilter(request, response, chain)
    }

    override fun doDelete(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
        doHttpFilter(request, response, chain)
    }

    private fun doHttpFilter(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
        val c = HttpContext(this, request, response, chain)
        try {
            app.beforeService(c)
            this.onService(c)
        } finally {
            app.afterService(c)
            c.clean()
            app.onCleanThreadLocal()
        }
    }

    private fun onService(c: HttpContext) {
        val lsReq = this.sliceList
        val ls = ArrayList<HttpSlice>()
        for (a in lsReq) {
            ls += a
            a.beforeRequest(c)
            if (c.isCommited) break
        }
        if (!c.isCommited) {
            doHttpAction(c, lsReq)
        }
        for (a in ls) {
            a.afterRequest(c)
        }
    }

    override fun onDestroy() {
        for (hs in sliceList) {
            hs.onDestory(this)
        }
        sliceList.clear()
        routeManager.onDestory()
        LogX.flush()
        LogX.clearPrinter()
        app.onCleanThreadLocal()
        app.onDestroy()
        ConnLook.shutdown()
        infoMap.clear()
        logd("HttpService.onDestroy()")
        println("HttpService.onDestroy()====================")
    }

    fun uriOf(action: HttpAction): String? {
        return routeManager.uriOf(action)
    }

    fun addSlice(hs: HttpSlice) {
        sliceList += hs
    }

    fun findRouter(block: (Router) -> Boolean): Router? {
        return routeManager.routeMap.values.firstOrNull(block)
    }

    fun addController(vararg clses: KClass<out HttpController>) {
        addController("", *clses)
    }

    fun addController(subpath: String, vararg clses: KClass<out HttpController>) {
        this.routeManager.addController(subpath, *clses)
    }

    @Suppress("UNCHECKED_CAST")
    fun scanControllers(pkg: String) {
        val ps = PackageScanner(pkg).scan()
        ps.classList.forEach {
            if (HttpController::class.java.isAssignableFrom(it)) {
                val kc: KClass<out HttpController> = it.kotlin as KClass<out HttpController>
                addController(kc)
            }
        }

    }

    companion object {
        val controllerSuffixs: HashSet<String> =
            hashSetOf("Controller", "Pages", "Page", "Apis", "Api", "Group", "PageGroup")
        const val APP_JSON = "app.json"
    }
}